﻿package com.yahoo.astra.fl.charts.series
{
	import com.yahoo.astra.fl.charts.Chart;
	import com.yahoo.astra.fl.charts.events.ChartEvent;
	
	import fl.core.InvalidationType;
	import fl.core.UIComponent;
	import fl.transitions.easing.Strong;
	
	import flash.display.DisplayObject;
	import flash.display.InteractiveObject;
	import flash.display.Shape;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;
		
	//--------------------------------------
	//  Styles
	//--------------------------------------
    
	/**
	 * The easing function for animations that occur on data changes.
	 */
    [Style(name="animationEasingFunction", type="Function")]
	
	/**
	 * The duration for animations that occur on data changes.
	 */
    [Style(name="animationDuration", type="int")]
	
	/**
	 * If true, data changes will be displayed with animations. If false, changes will happen instantly.
	 */
	[Style(name="animationEnabled", type="Boolean")]
    
	/**
	 * The base color used by objects displayed in this series.
	 */
    [Style(name="fillColor", type="Color")]
    
	/**
	 * The Class used to instantiate each marker's skin.
	 */
    [Style(name="markerSkin", type="Class")]
    
	/**
	 * The size, in pixels, of each marker.
	 */
    [Style(name="markerSize", type="Class")]

	/**
	 * Functionality common to most series. Generally, a <code>Series</code> object
	 * shouldn't be instantiated directly. Instead, a subclass with a concrete
	 * implementation should be used.
	 * 
	 * @author Josh Tynjala
	 */
	public class Series extends UIComponent implements ISeries
	{
		
	//--------------------------------------
	//  Class Variables
	//--------------------------------------
		
		/**
		 * @private
		 */
		private static var defaultStyles:Object = 
		{	
			markerSkin: Shape, //an empty display object
			fillColor: 0x00b8bf,
			markerSize: 10,
			animationEnabled: true,
			animationEasingFunction: fl.transitions.easing.Strong.easeOut,
			animationDuration: 500
		};
		
		/**
		 * @private
		 */
		private static const RENDERER_STYLES:Object = 
		{
			skin: "markerSkin",
			fillColor: "fillColor"
		};
		
	//--------------------------------------
	//  Class Methods
	//--------------------------------------
	
		/**
		 * @copy fl.core.UIComponent#getStyleDefinition()
		 */
		public static function getStyleDefinition():Object
		{
			return mergeStyles(defaultStyles, UIComponent.getStyleDefinition());
		}
		
	//--------------------------------------
	//  Constructor
	//--------------------------------------
	
		/**
		 * Constructor.
		 */
		public function Series(dataProvider:Object = null)
		{
			super();
			this._dataProvider = dataProvider;
		}
		
	//--------------------------------------
	//  Properties
	//--------------------------------------
		
		/**
		 * @private
		 */
		protected var markers:Array = [];
		
		/**
		 * @private
		 * A set of flags to indicate if special considerations need to be taken for the markers.
		 */
		protected var markerInvalidHash:Dictionary = new Dictionary(true);
		
		/**
		 * @private
		 * Storage for the chart property.
		 */
		private var _chart:Chart;
		
		/**
		 * @copy com.yahoo.astra.fl.charts.ISeries#chart
		 */
		public function get chart():Chart
		{
			return this._chart;
		}
		
		/**
		 * @private
		 */
		public function set chart(value:Chart):void
		{
			this._chart = value;
			//this is a fun hack to ensure that series know if their parent charts are in live preview
			if(this._chart == null || this._chart.parent == null)
			{
				this.isLivePreview = false;
			}
			var className:String;
			try
			{
				className = getQualifiedClassName(this._chart.parent);	
			}
			catch (e:Error) {}
			this.isLivePreview = (className == "fl.livepreview::LivePreviewParent");	
		}
		
		private var _itemToItemRendererHash:Dictionary = new Dictionary();
		
		/**
		 * @private
		 * Storage for the itemRenderer property.
		 */
		private var _itemRenderer:Object = SeriesItemRenderer;
		
		/**
		 * The class used to instantiate item renderers.
		 */
		public function get itemRenderer():Object
		{
			return this._itemRenderer;
		}
		
		/**
		 * @private
		 */
		public function set itemRenderer(value:Object):void
		{
			if(this._itemRenderer != value)
			{
				this._itemRenderer = value;
				this.invalidate("itemRenderer");
			}
		}
		
		/**
		 * @private
		 * Storage for the data property.
		 */
		private var _dataProvider:Object;
		
		/**
		 * @copy com.yahoo.astra.fl.charts.ISeries#data
		 */
		public function get dataProvider():Object
		{
			return this._dataProvider;
		}
		
		/**
		 * @private
		 */
		public function set dataProvider(value:Object):void
		{
			if(this._dataProvider != value)
			{
				//if we get XML data and it isn't an XMLList,
				//ignore the root tag
				if(value is XML && !(value is XMLList))
				{
					value = value.elements();
				}
				
				if(value is XMLList)
				{
					value = XMLList(value).copy();
				}
				else if(value is Array)
				{
					value = (value as Array).concat();
				}
				
				this._dataProvider = value;
				this.invalidate(InvalidationType.DATA);
				this.dispatchEvent(new Event("dataChange"));
			}
		}
		
		/**
		 * @private
		 * Storage for the displayName property.
		 */
		private var _displayName:String;
		
		/**
		 * @copy com.yahoo.astra.fl.charts.ISeries#data
		 */
		public function get displayName():String
		{
			return this._displayName;
		}
		
		/**
		 * @private
		 */
		public function set displayName(value:String):void
		{
			if(this._displayName != value)
			{
				this._displayName = value;
				this.invalidate(InvalidationType.DATA);
			}
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.ISeries#length
		 */
		public function get length():int
		{
			if(this._dataProvider is Array)
			{
				return (this._dataProvider as Array).length;
			}
			else if(this._dataProvider is XMLList)
			{
				return (this._dataProvider as XMLList).length();
			}
			
			return 0;
		}
		
	//--------------------------------------
	//  Public Methods
	//--------------------------------------
		
		/**
		 * @copy com.yahoo.astra.fl.charts.ISeries#clone()
		 */
		public function clone():ISeries
		{
			var series:Series = new Series();
			series.dataProvider = this.dataProvider;
			series.displayName = this.displayName;
			return series;
		}
		
		/**
		 * @copy com.yahoo.astra.fl.charts.ISeries#itemRendererToIndex()
		 */
		public function itemRendererToIndex(renderer:ISeriesItemRenderer):int
		{
			return this.markers.indexOf(renderer);
		}
		
		public function itemToItemRenderer(item:Object):ISeriesItemRenderer
		{
			return this._itemToItemRendererHash[item] as ISeriesItemRenderer;
		}
		
	//--------------------------------------
	//  Protected Methods
	//--------------------------------------
		
		/**
		 * @private
		 */
		override protected function draw():void
		{
			var itemRendererInvalid:Boolean = this.isInvalid("itemRenderer");
			var stylesInvalid:Boolean = this.isInvalid(InvalidationType.STYLES);
			var dataInvalid:Boolean = this.isInvalid(InvalidationType.DATA);
			
			//the class for the item renderers has changed. remove all markers
			//so that they may be recreated.
			if(itemRendererInvalid)
			{
				this.removeAllMarkers();
			}
			
			if(itemRendererInvalid || this.isInvalid(InvalidationType.DATA) || this.isInvalid(InvalidationType.STYLES))
			{
				this.refreshMarkers();
				this._itemToItemRendererHash = new Dictionary(true);
				var itemCount:int = this.markers.length;
				for(var i:int = 0; i < itemCount; i++)
				{
					var marker:ISeriesItemRenderer = this.markers[i] as ISeriesItemRenderer;
					if(dataInvalid) //update data if needed
					{
						marker.data = this.dataProvider[i];
					}
					this._itemToItemRendererHash[marker.data] = marker;
					
					var markerComponent:UIComponent = marker as UIComponent;
					if(stylesInvalid) //update styles if needed
					{
						this.copyStylesToChild(markerComponent, RENDERER_STYLES);
					}
					markerComponent.drawNow();
				}
			}
			
			super.draw();
		}
		
		/**
		 * @private
		 * All markers are removed from the display list.
		 */
		protected function removeAllMarkers():void
		{
			var markerCount:int = this.markers.length;
			for(var i:int = 0; i < markerCount; i++)
			{
				var marker:ISeriesItemRenderer = this.markers.pop() as ISeriesItemRenderer;
				marker.removeEventListener(MouseEvent.ROLL_OVER, markerRollOverHandler);
				marker.removeEventListener(MouseEvent.ROLL_OUT, markerRollOutHandler);
				marker.removeEventListener(MouseEvent.CLICK, markerClickHandler);
				marker.removeEventListener(MouseEvent.DOUBLE_CLICK, markerDoubleClickHandler);
				this.removeChild(DisplayObject(marker));
			}
		}
		
		/**
		 * @private
		 * Add or remove markers as needed. current markers will be reused.
		 */
		protected function refreshMarkers():void
		{
			var itemCount:int = this.length;
			var difference:int = itemCount - this.markers.length;
			if(difference > 0)
			{
				for(var i:int = 0; i < difference; i++)
				{
					var marker:ISeriesItemRenderer = new this.itemRenderer();
					marker.series = this;
					InteractiveObject(marker).doubleClickEnabled = true;
					marker.addEventListener(MouseEvent.ROLL_OVER, markerRollOverHandler, false, 0, true);
					marker.addEventListener(MouseEvent.ROLL_OUT, markerRollOutHandler, false, 0, true);
					marker.addEventListener(MouseEvent.CLICK, markerClickHandler, false, 0, true);
					marker.addEventListener(MouseEvent.DOUBLE_CLICK, markerDoubleClickHandler, false, 0, true);
					this.addChild(DisplayObject(marker));
					this.markers.push(marker);
					this.invalidateMarker(marker);
				}
			}
			else if(difference < 0)
			{
				difference = Math.abs(difference);
				for(i = 0; i < difference; i++)
				{
					marker = this.markers.pop() as ISeriesItemRenderer;
					this.validateMarker(marker);
					marker.removeEventListener(MouseEvent.ROLL_OVER, markerRollOverHandler);
					marker.removeEventListener(MouseEvent.ROLL_OUT, markerRollOutHandler);
					marker.removeEventListener(MouseEvent.CLICK, markerClickHandler);
					marker.removeEventListener(MouseEvent.DOUBLE_CLICK, markerDoubleClickHandler);
					this.removeChild(DisplayObject(marker));
				}
			}
			
			var markerCount:int = this.markers.length;
			for(i = 0; i < markerCount; i++)
			{
				marker = ISeriesItemRenderer(this.markers[i]);
				marker.data = this.dataProvider[i];
				this.copyStylesToChild(UIComponent(marker), RENDERER_STYLES);
			}
		}
		
		/**
		 * Indicates whether special considerations should be taken for a newly created marker.
		 */
		protected function isMarkerInvalid(marker:ISeriesItemRenderer):Boolean
		{
			return this.markerInvalidHash[marker];
		}
		
		protected function invalidateMarker(marker:ISeriesItemRenderer):void
		{
			markerInvalidHash[marker] = true;
			DisplayObject(marker).visible = false;
		}
		
		/**
		 * Makes a marker valid. To be used by subclasses.
		 */
		protected function validateMarker(marker:ISeriesItemRenderer):void
		{
			DisplayObject(marker).visible = true;
			delete markerInvalidHash[marker];
		}
		
		/**
		 * @private
		 * Notify the parent chart that the user's mouse is over this marker.
		 */
		protected function markerRollOverHandler(event:MouseEvent):void
		{
			var itemRenderer:ISeriesItemRenderer = ISeriesItemRenderer(event.currentTarget);
			var index:int = this.itemRendererToIndex(itemRenderer);
			var item:Object = this.dataProvider[index];
			var rollOver:ChartEvent = new ChartEvent(ChartEvent.ITEM_ROLL_OVER, index, item, itemRenderer, this);
			this.dispatchEvent(rollOver);
		}
		
		/**
		 * @private
		 * Notify the parent chart that the user's mouse has left this marker.
		 */
		protected function markerRollOutHandler(event:MouseEvent):void
		{			
			var itemRenderer:ISeriesItemRenderer = ISeriesItemRenderer(event.currentTarget);
			var index:int = this.itemRendererToIndex(itemRenderer);
			var item:Object = this.dataProvider[index];
			var rollOut:ChartEvent = new ChartEvent(ChartEvent.ITEM_ROLL_OUT, index, item, itemRenderer, this);
			this.dispatchEvent(rollOut);
		}
		
		/**
		 * @private
		 * Notify the parent chart that the user clicked this marker.
		 */
		protected function markerClickHandler(event:MouseEvent):void
		{
			var itemRenderer:ISeriesItemRenderer = ISeriesItemRenderer(event.currentTarget);
			var index:int = this.itemRendererToIndex(itemRenderer);
			var item:Object = this.dataProvider[index];
			var click:ChartEvent = new ChartEvent(ChartEvent.ITEM_CLICK, index, item, itemRenderer, this);
			this.dispatchEvent(click);
		}
		
		/**
		 * @private
		 * Notify the parent chart that the user double-clicked this marker.
		 */
		protected function markerDoubleClickHandler(event:MouseEvent):void
		{
			var itemRenderer:ISeriesItemRenderer = ISeriesItemRenderer(event.currentTarget);
			var index:int = this.itemRendererToIndex(itemRenderer);
			var item:Object = this.dataProvider[index];
			var doubleClick:ChartEvent = new ChartEvent(ChartEvent.ITEM_DOUBLE_CLICK, index, item, itemRenderer, this);
			this.dispatchEvent(doubleClick);
		}
	}
}